package fons.navigator.views.quadrant;

import java.awt.Color;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;

import edu.umd.cs.piccolo.PCamera;
import edu.umd.cs.piccolo.PNode;
import fons.model.ontology.AbstractView;
import fons.model.ontology.Context;
import fons.model.ontology.Dimension;
import fons.model.ontology.SelectionDefinition;
import fons.model.ontology.SelectionUpdateListener;
import fons.model.ontology.ValueSpace;
import fons.navigator.util.box2d.DynamicPlacer;
import fons.navigator.util.box2d.DynamicallyPlaceable;
import fons.navigator.util.piccolo.Legend;
import fons.navigator.util.piccolo.PTooltip;
import fons.navigator.views.quadrant.Spiral.ValuePair;

public class VisualSpiral extends PNode implements SelectionUpdateListener {
	private static final long serialVersionUID = 1L;
	
	private final SpiralView view;
	private final List<Spiral> spirals;
	private final AverageCross averageCross;
	private final ValueGrid valueGrid;
	private final PTooltip tooltip;
	
	private final Legend legend;
	
	private double xMinValue = 0;
	private double xMaxValue = 0;
	private double yMinValue = 0;
	private double yMaxValue = 0;
	private double xAvgValue = 0;
	private double yAvgValue = 0;
	
	private double xMinProjection = 0;
	private double xSpanProjection = 200;
	private double yMinProjection = 0;
	private double ySpanProjection = -200;
	
	private final Context context;
	private final Set<Dimension> xFilters = Sets.newHashSet();
	private final Set<Dimension> yFilters = Sets.newHashSet();
	private final Set<Dimension> valueDimensions = Sets.newTreeSet(Dimension.COMPARE_BY_DEFINITION);
	private final Set<Dimension> commonFilters;
	
	private boolean valid = false;
	
	private final PropertyChangeListener propertyChangeEventListener;
	private final DynamicPlacer dynamicPlacer = new DynamicPlacer();
	
	public VisualSpiral (final SpiralView view, final Context context,
			final Set<Dimension> targets, 
			final Set<Dimension> values, 
			final Dimension xAxis, final Dimension yAxis, 
			Set<Dimension> xFilters, Set<Dimension> yFilters) {
		
		this.view = view;
		this.context = context;
		
		this.spirals = new Vector<Spiral>();
		this.tooltip = new PTooltip();
		
		this.valueDimensions.addAll(values);
		this.xFilters.addAll(xFilters);
		this.xFilters.add(xAxis);
		this.yFilters.addAll(yFilters);
		this.yFilters.add(yAxis);
		this.commonFilters = Sets.intersection(xFilters, yFilters);
		
		final ValueSpace filterSpace = ValueSpace.of(commonFilters);
		
		for (Dimension target: targets) {
			final Spiral spiral = new Spiral(target, filterSpace.subSpace(target), this);
			spirals.add(spiral);
			if (spiral.isValid()) {
				getCamera().addChild(spiral);
				this.valid = true;
			}
		}
			
		calculateStats();
		
		this.valueGrid = new ValueGrid(this);
		getCamera().addChild(valueGrid);
		
		this.averageCross = new AverageCross(this);
		addChild(averageCross);
						
		this.propertyChangeEventListener = new PropertyChangeListener() {
			public void propertyChange(PropertyChangeEvent evt) {
				if (evt.getPropertyName() == PCamera.PROPERTY_BOUNDS) {
			        //fitInCameraView();
			        valueGrid.update();
					
				} else if (evt.getPropertyName() == PCamera.PROPERTY_VIEW_TRANSFORM) {
					updateSpiralPositions();
					valueGrid.update();
				}
			}
		};
		getCamera().addPropertyChangeListener(this.propertyChangeEventListener);

		this.legend = new Legend(
		        new Color(240, 240, 240, 200),
		        new SpiralLegendItemProducer(this));
		getCamera().addChild(legend);
		
		context.getStandardSelection().addSelectionUpdateListener(this);
		
		tooltip.setVisible(false);
		getCamera().addChild(tooltip);		
		
		this.fitInCameraView();
	}


	private void updateSpiralPositions() {
		for (Spiral spiral: spirals) {
			spiral.updatePositionInCamera(getCamera());
		}
	}

	public void calculatePositions () {
		dynamicPlacer.clear();
		
		for (Spiral spiral: spirals) {
			if (spiral.isSelected()) {
				for (DynamicallyPlaceable dp: spiral.getPlaceables()) {
					if (dp.getVisible())
						dynamicPlacer.add(dp);
				}
			}
		}
		
		dynamicPlacer.calculatePositions();
		dynamicPlacer.updatePositions();
	}
	
	public Context getContext() {
		return context;
	}

	public AbstractView getAbstractView() {
		return view;
	}

	public List<Spiral> getSpirals() {
		return Collections.unmodifiableList(spirals);
	}

	public void fitInCameraView () {
		getCamera().setViewBounds(new Rectangle2D.Double(getXMinProjection(), getYMinProjection(),
				getXMaxProjection(), getYMaxProjection()));
	}
	
	private void calculateStats () {
		int countX = 0;
		int countY = 0;
		for (Spiral spiral: spirals) {
			for (Spiral.ValuePair values: spiral.getValuePairs()) {
				if (values.isValid()) {
					final double x = values.getX().getValue();
					final double y = values.getY().getValue();
					
					if (x < xMinValue) xMinValue = x;
					if (x > xMaxValue) xMaxValue = x;
					if (y < yMinValue) yMinValue = y;
					if (y > yMaxValue) yMaxValue = y;
					
					xAvgValue += x;
					countX++;
					
					yAvgValue += y;
					countY++;
				}
			}
		}
		xAvgValue /= countX;
		yAvgValue /= countY;
		
		double s_xx = 0.0;
		double s_yy = 0.0;
		double s_xy = 0.0;
		
		for (Spiral spiral: spirals) {
			for (Spiral.ValuePair values: spiral.getValuePairs()) {
				double x = spiral.getAverageX();
				double y = spiral.getAverageY();

				if (Double.isNaN(x) || Double.isNaN(y)) continue;
				if (values.isValid()) {							
					s_xx += (x - xAvgValue) * (x - xAvgValue);
					s_yy += (y - yAvgValue) * (y - yAvgValue);		
					s_xy += (x - xAvgValue) * (y - yAvgValue);
				}
			}
		}
		
		this.regXb = s_xy / s_xx;
		this.regYb = s_xy / s_yy;
	}
	
	private double regXb;
	private double regYb;
	
	public boolean isValid() {
		return this.valid;
	}
	
	protected PCamera getCamera() {
		return view.getVisualCanvas().getCamera();
	}

	public double getXMinValue() {
		return xMinValue;
	}

	public double getXMaxValue() {
		return xMaxValue;
	}
	
	public double getXSpanValue() {
		return xMaxValue - xMinValue;
	}

	public double getYMinValue() {
		return yMinValue;
	}

	public double getYMaxValue() {
		return yMaxValue;
	}
	
	public double getYSpanValue() {
		return yMaxValue - yMinValue;
	}

	public double getXAvgValue() {
		return xAvgValue;
	}

	public double getYAvgValue() {
		return yAvgValue;
	}

	public double getXMinProjection() {
		return xMinProjection;
	}
	
	public double getXMaxProjection() {
		return xMinProjection + xSpanProjection;
	}

	public double getXSpanProjection() {
		return xSpanProjection;
	}

	public double getYMinProjection() {
		return yMinProjection;
	}
	
	public double getYMaxProjection() {
		return yMinProjection + ySpanProjection;
	}

	public double getYSpanProjection() {
		return ySpanProjection;
	}
	
	public double xToProjection (final double xValue) {
		return xMinProjection + (xValue - xMinValue) * (xSpanProjection) / getXSpanValue();
	}
	
	public double yToProjection (final double yValue) {
		return yMinProjection + (yValue - yMinValue) * (ySpanProjection) / getYSpanValue();
	}
	
	public Point2D toProjection (final Point2D values) {
		values.setLocation(xToProjection(values.getX()), yToProjection(values.getY()));
		return values;
	}
	
	public Point2D newProjectionFrom (final Point2D values) {
		return toProjection((Point2D)values.clone());
	}
	
	public double xToValue (final double xProjection) {
		return xMinValue + (xProjection - xMinProjection) * (getXSpanValue()) / xSpanProjection;
	}
	
	public double yToValue (final double yProjection) {
		return yMinValue + (yProjection - yMinProjection) * (getYSpanValue()) / ySpanProjection;
	}
	
	public Point2D toValue (final Point2D projections) {
		projections.setLocation(xToValue(projections.getX()), yToValue(projections.getY()));
		return projections;
	}

	public Legend getLegend() {
		return legend;
	}

	public void removeNodesFromCamera() {
		PCamera cam = getCamera();
		
		for (Spiral s: getSpirals()) {
			cam.removeChild(s);
		}
		cam.removeChild(valueGrid);
		cam.removeChild(legend);
		cam.removeChild(tooltip);
		cam.removePropertyChangeListener(this.propertyChangeEventListener);
		
		averageCross.removeNodesFromCamera();		
	}
	
	public void showTooltip (Spiral spiral, Point2D pos) {
		
		StringBuilder labels = new StringBuilder();
		StringBuilder values = new StringBuilder();
		
		final Map<Dimension, Spiral.ValuePair> valuePairs = spiral.getValues();
		final List<Dimension> sorted = Lists.newArrayList(valuePairs.keySet());
		Collections.sort(sorted, Ordering.usingToString());
		
		for (final Dimension valueDimension: sorted) {
			final ValuePair valuePair = valuePairs.get(valueDimension);
			final Point2D value = valuePair.toPoint2D();
			
			if (!valuePair.isValid()) continue;
			
			labels.append(valueDimension.getName());
			labels.append(": \n\n");
			
			values.append(Math.round(value.getX() * 100) / 100.0);
			values.append(" (");
			boolean first = true;
			for (Dimension different: Sets.difference(xFilters, yFilters)) {
				if (!first) values.append(", ");
				values.append(different.getName());
				first = false;
			}
			values.append(")\n");
			
			values.append(Math.round(value.getY() * 100) / 100.0);
			values.append(" (");
			first = true;
			for (Dimension different: Sets.difference(yFilters, xFilters)) {
				if (!first) values.append(", ");
				values.append(different.getName());
				first = false;
			}
			values.append(")\n");
		}
		
		tooltip.setText(spiral.getLabel(), labels.toString(), values.toString());
		tooltip.showTooltipAt(pos.getX(), pos.getY(), 8, 8);
		tooltip.setVisible(true);
	}

	public void hideTooltip() {
		tooltip.setVisible(false);
	}

	public AverageCross getAverageCross() {
		return averageCross;
	}

	public ValueGrid getValueGrid() {
		return valueGrid;
	}

	@Override
	public void beforeSelectionChanged() {
	}

	@Override
	public void afterSelectionChanged() {
		this.calculatePositions();
	}

	@Override
	public void dispose() {
		context.getStandardSelection().removeSelectionUpdateListener(this);
		
		for (final Spiral spiral: this.spirals) {
			spiral.dispose();
		}
	}

	@Override
	public void selectedSelection(SelectionDefinition definition) {
	}

	public double getRegXb() {
		return regXb;
	}

	public double getRegYb() {
		return regYb;
	}

	public Set<Dimension> getXFilters() {
		return xFilters;
	}

	public Set<Dimension> getYFilters() {
		return yFilters;
	}
	
	public Set<Dimension> getCommonFilters() {
		return commonFilters;
	}
	
	public Set<Dimension> getValueDimensions() {
		return valueDimensions;
	}
}
